//
// JavaTemplar
//
// Copyright (C) jextra.net.
//
//  This file is part of the JavaTemplar Library.
//
//  The JavaTemplar Library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The JavaTemplar Library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the JavaTemplar Library; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.javatemplar;

import java.io.*;
import java.util.*;
import java.util.regex.*;
import net.jextra.javatemplar.token.*;

public class JavaTemplar
{
    // ============================================================
    // Fields
    // ============================================================

    public static String ROOT_BLOCK_NAME = "<<ROOT BLOCK>>";
    private static Pattern _beginPattern;
    private static Pattern _endPattern;
    private static Pattern _variablePattern;

    private Reader _in;
    private boolean _strict;
    private int _lineNum;

    // A root placeholder for all of the tokens.
    private JTBlockStencilToken _rootStencil;

    private Stack<JTBlockStencilToken> _stencilStack;

    // ============================================================
    // Constructors
    // ============================================================

    public JavaTemplar( File file )
        throws JavaTemplarException
    {
        FileReader in;
        try
        {
            in = new FileReader( file );
            read( in );
            in.close();
        }
        catch ( Exception ex )
        {
            throw new JavaTemplarException( ex );
        }
    }

    public JavaTemplar( Reader in )
        throws JavaTemplarException
    {
        read( in );
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    /**
     * Determines if all variables must be set when templatizing.  Throws exceptions if true and a variable is not set.
     */
    public void setIsStrict( boolean isStrict )
    {
        _strict = isStrict;
    }

    public void read( Reader reader )
        throws JavaTemplarException
    {
        try
        {
            init( reader );

            BufferedReader in = new BufferedReader( reader );
            _lineNum = 1;
            for ( String line = in.readLine(); line != null; line = in.readLine() )
            {
                processString( true, line + "\n" );
                _lineNum++;
            }

            // When everything is done, make a single instance block from the root stencil.
            _rootStencil.makeBlock();
        }
        catch ( Exception ex )
        {
            throw new JavaTemplarException( ex );
        }
    }

    public void close()
        throws IOException
    {
        _in.close();
    }

    /**
     * Set a variable with the given name.
     */
    public void setVariable( String name, String value )
        throws JavaTemplarException
    {
        if ( name == null )
        {
            throw new JavaTemplarException( "A variable name can not be null." );
        }

        name = name.trim();
        if ( name.isEmpty() )
        {
            throw new JavaTemplarException( "A variable name can not be empty." );
        }

        if ( name.contains( " " ) )
        {
            throw new JavaTemplarException( "Variable names can not contain spaces." );
        }

        if ( _rootStencil == null )
        {
            throw new JavaTemplarException( "Template must be read first before setting " +
                "variables." );
        }

        _rootStencil.setVariable( name, value );
    }

    public void addBlock( String blockName )
        throws JavaTemplarException
    {
        boolean found = _rootStencil.makeBlock( blockName );
        if ( !found )
        {
            throw new JavaTemplarException( "There is not a block in the template with the name '" +
                blockName + "'." );
        }
    }

    public void write( Writer out )
        throws JavaTemplarException
    {
        try
        {
            _rootStencil.write( out );
        }
        catch ( Exception ex )
        {
            throw new JavaTemplarException( ex );
        }
    }

    @Override
    public String toString()
    {
        try
        {
            StringWriter writer = new StringWriter();
            write( writer );

            return writer.toString();
        }
        catch ( JavaTemplarException ex )
        {
            throw new RuntimeException( ex );
        }
    }

    // ----------
    // private
    // ----------

    private void init( Reader in )
    {
        _in = in;

        // Initialize patterns if not done so yet.
        if ( _beginPattern == null )
        {
            _beginPattern = Pattern.compile( "([^$]*)\\$\\{begin\\s([^}]*)\\}(.*)",
                Pattern.CASE_INSENSITIVE | Pattern.DOTALL );
            _endPattern = Pattern.compile( "([^$]*)\\$\\{end\\s([^}]*)\\}(.*)",
                Pattern.CASE_INSENSITIVE | Pattern.DOTALL );
            _variablePattern = Pattern.compile( "([^$]*)\\$\\{([^}]*)\\}(.*)",
                Pattern.CASE_INSENSITIVE | Pattern.DOTALL );
        }

        _stencilStack = new Stack<JTBlockStencilToken>();
        _rootStencil = new JTBlockStencilToken( null, ROOT_BLOCK_NAME );
        _stencilStack.push( _rootStencil );
    }

    private void processString( boolean flag, String string )
        throws JavaTemplarException
    {
        Matcher beginMatcher = _beginPattern.matcher( string );
        Matcher endMatcher = _endPattern.matcher( string );
        Matcher varMatcher = _variablePattern.matcher( string );

        JTBlockStencilToken currentStencil = _stencilStack.peek();

        if ( beginMatcher.matches() )
        {
            processString( flag, beginMatcher.group( 1 ) );

            String name = beginMatcher.group( 2 ).trim();
            if ( name.isEmpty() )
            {
                throw new JavaTemplarException( "Line " + _lineNum +
                    ": Block name can not be null." );
            }
            else if ( name.contains( " " ) )
            {
                throw new JavaTemplarException( "Line " + _lineNum +
                    ": Block name can not contain spaces." );
            }

            JTBlockStencilToken stencil = new JTBlockStencilToken( currentStencil, name );
            currentStencil.addToken( stencil );
            _stencilStack.push( stencil );

            // If nothing on line other than this, ignore the return.
            processTail( flag, beginMatcher.group( 1 ), beginMatcher.group( 3 ) );
        }
        else if ( endMatcher.matches() )
        {
            processString( flag, endMatcher.group( 1 ) );

            JTBlockStencilToken stencil = _stencilStack.pop();
            if ( !stencil.getName().equals( endMatcher.group( 2 ).trim() ) )
            {
                throw new JavaTemplarException( "Line " + _lineNum + ": '" + stencil.getName() +
                    "' != '" + endMatcher.group( 2 ) + "'." );
            }

            // If nothing on line other than this, ignore the return.
            processTail( flag, endMatcher.group( 1 ), endMatcher.group( 3 ) );
        }
        else if ( varMatcher.matches() )
        {
            processString( flag, varMatcher.group( 1 ) );

            String name = varMatcher.group( 2 ).trim();
            if ( name.isEmpty() )
            {
                throw new JavaTemplarException( "Line " + _lineNum +
                    ": Variable name can not be null." );
            }
            else if ( name.contains( " " ) )
            {
                throw new JavaTemplarException( "Line " + _lineNum +
                    ": Variable name can not contain spaces." );
            }

            currentStencil.addToken( new JTVariableToken( name, null, _strict ) );
            processString( false, varMatcher.group( 3 ) );
        }
        else
        {
            currentStencil.addToken( new JTTextToken( string ) );
        }
    }

    /**
     * Special processing to take care of tail of line unless only thing left is a \n, in
     * which case, ignore it.
     */
    private void processTail( boolean flag, String before, String after )
        throws JavaTemplarException
    {
        // Don't let trim() remove \n.
        String trail = after.replace( '\n', '?' ).trim();

        if ( !flag || !before.trim().isEmpty() || !trail.equals( "?" ) )
        {
            processString( false, after );
        }
    }
}

